fix(mesh-store): converge state sync by per-entity version - #29
Merged
Conversation
The state_sync receive path merged add-only, so an entity that changed while a peer was fully disconnected could never be corrected by a sync: the returning peer kept its stale copy, and a peer receiving that stale snapshot kept whatever it already had. Room member lists had the mirror problem — the union merge could add members but never remove a leaver. Give agents and rooms a monotonic revision the mutating store bumps and embeds in the record. Merges (both state_sync snapshots and incremental patches) are now tri-state: a strictly higher version replaces the record wholesale (which is what heals renames, edits, and leaves), an equal version unions memberships and subscriptions (so concurrent joins from the same base both survive), and a lower version is rejected (so a stale holder cannot regress a current one). Message and DM histories merge by message id, adding unseen entries and unioning read receipts. Owner-authority merging was rejected during design: a rejoining peer can carry an old copy of a third peer's entity and would clobber it. The merge itself is extracted to a public applyStateSync seam so the convergence contract is unit-testable without transports; the tests cover both directions (stale holder heals, stale snapshot rejected), room membership healing including leaves, and history/read-receipt merging. The wire format carries the version inside the entity records, so a mesh must be on a single build; the serial number fix and this change ship together.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
This was referenced Sep 9, 2026
Closed
Mearman
added a commit
that referenced
this pull request
Sep 10, 2026
A legacy peer's state_sync payload can genuinely be missing deliveryQueues (#30) or an entity's version field (#29), since nothing validates a state_sync message's shape beyond isMeshMessage's bare method check. normaliseWireState fills in the defaults (empty collections, version 1) before the payload reaches applyStateSync, so an old-build snapshot parses to a complete state instead of throwing. WireStateInput models exactly that tolerance as its own type rather than widening SerialisedState itself, so domain code elsewhere keeps working with the fully-populated type.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #27.
The
state_syncreceive path merged add-only, so an entity that changed while a peer was fully disconnected could never be corrected by a sync — the returning peer kept its stale copy, and anyone receiving that stale snapshot kept whatever they already had. Room member lists had the mirror problem: the union merge could add members but never remove a leaver.Agents and rooms now carry a monotonic revision the mutating store bumps and embeds in the record. Merges — both snapshot syncs and incremental patches — are tri-state:
Message and DM histories merge by message id, adding unseen entries and unioning read receipts. Owner-authority merging was considered and rejected during design (a rejoining peer can carry an old copy of a third peer's entity and would clobber it); the reasoning is on #27.
The merge is extracted to a public
applyStateSync(state)seam so the convergence contract is unit-testable without transports: stale holder heals, stale snapshot rejected, room membership healing including leaves, and history/read-receipt merging. Wired intopnpm test(17 tests total, five clean consecutive rounds during development plus lint/typecheck/build).Note: the version travels inside the entity records, so a mesh needs all peers on builds that understand it — no wire negotiation, same as the rest of the protocol.